package edu.ntnu.controller;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Class for recording robot's movement and access the map
 * This class is singleton
 */
public class MapBuilder {

	private static MapBuilder singleton;
	private Map map;
	private Point position;
	private int orientation;
	
	private MapBuilder() {
		map = new Map(Map.MAX_WIDTH, Map.MAX_WIDTH);
		position = new Point(Map.MAX_WIDTH/2 * Map.SCALE, Map.MAX_WIDTH/2 * Map.SCALE);
		orientation = 0;
		record(Map.WAY);
	}
	
	public static synchronized MapBuilder getNewInstance() {
		singleton = new MapBuilder();
		return singleton;
	}
	
	/**
	 * singleton constructor
	 * @return	singleton of MapBuilder
	 */
	public static synchronized MapBuilder getInstance() {
		if (singleton == null) {
			singleton = new MapBuilder();
		}
		return singleton;
	}

	/**
	 * checks area in given direction of robot
	 * if robot has already been there, area is explored and method returns true
	 * @param direction	given direction from robot
	 * @return			true, if area is already explored, false otherwise
	 */
	public boolean isExplored(int direction) {
		Point p = position.getAnother(orientation + direction, 250);
		for(int offset = -100; offset < 200; offset ++) {
			if (map.get(p.getAnother(orientation + direction + 30, offset)) != Map.UNKNOWN ||
				map.get(p.getAnother(orientation + direction - 30, offset)) != Map.UNKNOWN ) {
				return true;
			}
		}
		
		return false;
	}

	/**
	 * finds where in the map is light located and returns position of it
	 * @return	position of light in the map if it was found, null otherwise
	 */
	public Point findLight() {
		for(int x = 0; x < Map.MAX_WIDTH; x++) {
			for(int y = 0; y < Map.MAX_WIDTH; y++) {
				Point p = new Point(x * Map.SCALE, y * Map.SCALE);
				if(map.get(p) == Map.LIGHT) {
					return p;
				}
			}
		}
		return null;
	}

	/**
	 * finds where in the map are balls located and returns positions of them
	 * @return	positions of balls that were found in the map
	 */
	public List<Point> findBalls() {
		List<Point> balls = new ArrayList<Point>();
		for(int x = 0; x < Map.MAX_WIDTH; x++) {
			for(int y = 0; y < Map.MAX_WIDTH; y++) {
				Point p = new Point(x * Map.SCALE, y * Map.SCALE);
				if(map.get(p) == Map.BALL) {
					balls.add(p);
				}
			}
		}
		return balls;
	}
	
	public synchronized Point getPosition() {
		return position;
	}
	  
	/**
	 * Set new position of robot and record way from previous position.
	 * @param newPosition	new robot's position
	 */
	public synchronized void setPosition(Point newPosition) {
		// TODO: check whether (int)(3/2) == 2
		double xOffset = newPosition.getX() - position.getX();
		double yOffset = newPosition.getY() - position.getY();
		double maxOffset = Math.max(Math.abs(xOffset), Math.abs(yOffset));
		Point way = new Point(position);
		  
		for (double i = 1; i <= maxOffset; i++) {
			way.setX(position.getX() + (int)(xOffset / maxOffset * i));
			way.setY(position.getY() + (int)(yOffset / maxOffset * i));
			map.add(way, Map.WAY);
		}
		  
		position = newPosition;
	}

	public synchronized int getOrientation() {
		return orientation;
	}

	public synchronized void setOrientation(int newOrientation) {
		while(newOrientation < 0) {
			newOrientation = newOrientation + 360;
		}
		orientation = newOrientation % 360;
	}
	
	public void record(int value) {
		map.add(position, value);
	}
	
	/**
	 * Finds shortest way from actual position to the end position
	 * @param end	given end position
	 * @return		list of crossroads on the shortest way to the end position
	 * @throws NavigationException 
	 */
	public List<Point> findWay(Point end) throws NavigationException {
		
		// check parameter
		if (end == null) {
			throw new NavigationException("end point cannot be null");
		}
		
		List<Point> way = dijkstra(position, end);
		way.add(end);
		return way;
	}
	/**
	 * According Dijkstra's algorithm finds the shortest way from given starting point (node)
	 * to ending point (node) in the map (graph).
	 * @param start	start point
	 * @param end	end point
	 * @return		set of points from start to end
	 * @throws NavigationException 
	 */
	public List<Point> dijkstra(Point start, Point end) throws NavigationException {
		
		// check parameters
		if (start == null || end == null) {
			throw new NavigationException("start and end point cannot be null");
		}
		
		// initialization 
		List<Point> nodes = new ArrayList<Point>();
		start.set(start.getX() - start.getX() % Map.SCALE, start.getY() - start.getY() % Map.SCALE);
		end.set(end.getX() - end.getX() % Map.SCALE, end.getY() - end.getY() % Map.SCALE);
		java.util.Map<Point, Integer> distances = new HashMap<Point, Integer>();
		for(int x = 0; x < Map.MAX_WIDTH; x++) {
			for(int y = 0; y < Map.MAX_WIDTH; y++) {
				Point p = new Point(x * Map.SCALE, y * Map.SCALE);
				if(map.get(p) == Map.BALL || map.get(p) == Map.WAY || map.get(p) == Map.LIGHT) {
					nodes.add(p);
				}
			}
		}

		// default values
		int distance = 0;
		nodes.remove(start);
		distances.put(start, distance);
		
		// marking nodes
		for(distance = 0; nodes.contains(end); distance++) {
			Set<Point> lastAddNodes = new HashSet<Point>();
			for(Point node : distances.keySet()) {
				if (distances.get(node) == distance) {
					lastAddNodes.add(node);
				}
			}
			for(Point node : lastAddNodes) {
				for(int angle = 0; angle < 360; angle = angle + 90) {
					Point nextNode = node.getAnother(angle, Map.SCALE);
					if(nodes.remove(nextNode)) {
						distances.put(nextNode, distance + 1);
					}
				}
			}
			if (lastAddNodes.isEmpty()) {
				throw new NavigationException("way was not found");
			}
		}
		
		// set up way
		List<Point> way = new ArrayList<Point>();
		Point node = end;
		int tempOrientation = 1; // is not equal with any possible angel

		for (; distance > 0; distance--) {
			for(int angle = 0; angle < 360; angle = angle + 90) {
				Point previousNode = node.getAnother(angle, Map.SCALE);
				if(distances.get(previousNode) != null && distances.get(previousNode) == distance - 1) {
					if (angle != tempOrientation) {
						tempOrientation = angle;
						way.add(0, node);
					}
					node = previousNode;
					break;
				}
			}
		}
		return way;
	}
	
	public void saveMap() {
		map.save();
	}
}
